import { BaseHttpClient, BaseHttpClientOptions, defaultBaseHttpClientOptions, IHttpProxy, TransportOptions } from "tsrpc-base-client";
import { ApiReturn, BaseServiceType, ServiceProto, TsrpcError } from "tsrpc-proto";

/**
 * HTTP Client for TSRPC.
 * It uses XMLHttpRequest to send requests.
 * @typeParam ServiceType - `ServiceType` from generated `proto.ts`
 */
export class HttpClient<ServiceType extends BaseServiceType> extends BaseHttpClient<ServiceType> {

    // readonly options: Readonly<HttpClientOptions>;

    constructor(proto: ServiceProto<ServiceType>, options?: Partial<HttpClientOptions>) {
        let httpProxy = new HttpProxy();
        super(proto, httpProxy, {
            ...defaultHttpClientOptions,
            ...options
        });
    }

    callApi<T extends string & keyof ServiceType['api']>(apiName: T, req: ServiceType['api'][T]['req'], options: HttpClientTransportOptions = {}): Promise<ApiReturn<ServiceType['api'][T]['res']>> {
        return super.callApi(apiName, req, options);
    };

    sendMsg<T extends string & keyof ServiceType['msg']>(msgName: T, msg: ServiceType['msg'][T], options: HttpClientTransportOptions = {}): Promise<{ isSucc: true } | { isSucc: false, err: TsrpcError }> {
        return super.sendMsg(msgName, msg, options);
    }

}

export interface HttpClientTransportOptions extends TransportOptions {
    /**
     * Event when progress of data sent is changed
     * @param ratio - 0~1
     */
    onProgress?: (ratio: number) => void;
}

const defaultHttpClientOptions: HttpClientOptions = {
    ...defaultBaseHttpClientOptions,
    customObjectIdClass: String,
}

export interface HttpClientOptions extends BaseHttpClientOptions {

}


class HttpProxy implements IHttpProxy {

    fetch(options: {
        url: string;
        data: string | Uint8Array;
        method: string;
        /** ms */
        timeout?: number;
        headers?: {
            [key: string]: string;
        };
        transportOptions: HttpClientTransportOptions;
        responseType: 'text' | 'arraybuffer';
    }): {
        abort: () => void;
        promise: Promise<{
            isSucc: true;
            res: string | Uint8Array;
        } | {
            isSucc: false;
            err: TsrpcError;
        }>;
    } {

        let rs: (v: { isSucc: true, res: string | Uint8Array } | { isSucc: false, err: TsrpcError }) => void;
        let promise: ReturnType<IHttpProxy['fetch']>['promise'] = new Promise(resolve => rs = resolve);

        let xhr = new XMLHttpRequest();


        xhr.onreadystatechange = () => {
            if (xhr.readyState === 4) {
                rs({
                    isSucc: true,
                    res: xhr.response && (options.responseType === 'text' ? xhr.responseText : new Uint8Array(xhr.response as ArrayBuffer))
                })
            }
        }

        xhr.onerror = () => {
            rs({
                isSucc: false,
                err: new TsrpcError({
                    message: 'Network Error',
                    type: TsrpcError.Type.NetworkError,
                    httpCode: xhr.status
                })
            });
        }

        xhr.ontimeout = () => {
            rs({
                isSucc: false,
                err: new TsrpcError({
                    message: 'Request Timeout',
                    type: TsrpcError.Type.NetworkError,
                    code: 'TIMEOUT'
                })
            });
        }

        // Res
        xhr.onload = async () => {
            if (xhr.status === 200 || xhr.status === 500) {

                rs({
                    isSucc: true,
                    res: xhr.response && (options.responseType === 'text' ? xhr.responseText : new Uint8Array(xhr.response as ArrayBuffer))
                })

            } else {

                rs({
                    isSucc: false,
                    err: new TsrpcError({
                        message: 'HTTP Error ' + xhr.status,
                        type: TsrpcError.Type.ServerError,
                        httpCode: xhr.status
                    })
                });
            }
        }

        let transportOptions = options.transportOptions;
        if (!!transportOptions.onProgress) {
            xhr.upload.onprogress = e => {
                transportOptions.onProgress?.(e.loaded / e.total);
            }
        }

        xhr.open(options.method.toLowerCase(), options.url, true);

        if (options.headers) {
            for (let key in options.headers) {
                xhr.setRequestHeader(key, options.headers[key]);
            }
        }
        xhr.responseType = options.responseType;
        let timeout = options.timeout;
        if (timeout) {
            xhr.timeout = timeout;
        }

        xhr.send(options.data || null);
        let abort = xhr.abort.bind(xhr);

        return {
            promise: promise,
            abort: abort
        }
    }
}